查看原文
其他

Android 15?我想躺着

Zhujiang 郭霖
2024-07-19


/   今日科技快讯   /

近日消息,业内人士爆料,苹果正在开发Vision Pro廉价版头显,代号N107,这款设备最快会在2025年底登场。与Vision Pro相比,廉价版头显最大变化是不能单独使用,必须连接iPhone或者Mac设备。


Vision Pro搭载M2和R1芯片,其中R1是为应对实时传感器处理任务而设计的,它负责处理来自12个摄像头、5个传感器和6个麦克风的数据,M2芯片负责Vision Pro自身的运转性能,因此Vision Pro可以独立使用。



/   作者简介   /

本篇文章转自Zhujiang的博客,文章主要分享了Android15中的新变化及相关适配,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7376622203301462055

/   前言   /

本篇文章也会从以下几个点切入:

  • 涉及所有应用的变更
  • 针对Android 15应用的变更
  • Android 15新功能探索
  • Android 15 时间线及总结

    /   涉及所有应用的变更   /

    Support for 16 KB page sizes


    之前的 Android 版本中仅支持 4 KB 页面内存,这优项修改化了系统内存性能,使其适用于 Android 设备通常拥有的平均总内存量。从 Android 15 开始,Android 支持配置为使用 16 KB 页面大小的设备。

    • 系统面临内存压力时缩短应用启动时间:平均缩短 3.16%,谷歌测试的一些应用的改进更为显著(高达 30%)
    • 降低应用程序启动时的耗电量:平均减少 4.56%
    • 相机启动速度更快:热启动速度平均加快 4.48%
    • 冷启动速度平均加快 6.60%
    • 改善系统启动时间:平均改善1.5%(约0.8秒)

    如果应用使用的是 Java 或 Kotlin 构建的话,则无需做处理,可以直接适配,但如果应用使用任何NDK库(无论是直接使用还是通过 SDK 间接使用),则需要重新构建应用,使其在这些 16 KB 设备上运行。

    构建文档地址:
    https://developer.android.com/guide/practices/page-sizes#build

    隐私空间


    隐私空间是 Android 15 中的一项新功能,用户可在其设备上创建一个单独的空间,通过额外的身份验证,让敏感应用远离窥探。由于隐私空间中的应用可见性受到限制,因此某些类型的应用需要采取额外步骤才能查看用户隐私空间中的应用并与之交互。由于私人空间中的应用保存在单独的用户配置文件中(类似于工作配置文件),因此应用不应假设其应用的任何已安装副本(不在主配置文件中)都在工作配置文件中。

    这块大部分开发者无需考虑,和应用没有什么关系,但对 Launcher 和应用商店有些影响,需要坐下适配。

    Launcher


    如果在开发 Launcher ,则必须先执行以下操作,然后隐私空间中的应用才可见:

    1. 开发的 Launcher 必须被指定为设备的默认应用,即拥有该 ROLE_HOME 角色。
    2. 必须 在应用的清单文件中ACCESS_HIDDEN_PROFILES 声明正常权限。

    声明该ACCESS_HIDDEN_PROFILES权限 Launcher 必须处理以下私有空间用例:

    1. 必须有一个单独的启动器容器,用于安装位于私人空间的应用。
    2. 用户必须能够隐藏和显示私人空间容器。
    3. 用户必须能够锁定和解锁私人空间容器。
    4. 锁定期间,私人空间容器内的任何应用都不应可见,或无法通过搜索等机制发现。


    应用商店应用


    隐私空间中包含一个“安装应用”按钮,该按钮会启动隐式 Intent 以将应用安装到用户的私人空间中。需要在应用的清单文件中声明一个带有 CATEGORY_APP_MARKET 的 ""。

    这个东西怎么说呢,冠冕堂皇,看着好像是给用户一些隐私,但隐私有直接在页面中展示的么?难道不应该是通过一些用户自定义的特殊操作才能触发的才叫隐私么?不过有比没有强,相信国内的各大厂商会把这个功能做的更好,真正成为“隐私空间”。


    MinSDK 更新


    MinSDK 版本从 23 增加到了 24,大家都知道,Android 14 中将 MinSDK 改为了23,想要彻底杜绝应用设置 SDK 版本为 23 以下从而绕开动态权限,这回又升级了一下,应用想绕过 FileProvider 处理一些文件也不可以了,之后肯定会越来越高,这样挺好,能杜绝一些流氓应用。

    如果需要测试针对旧 API 级别的应用,可以使用以下 ADB 命令:

    adb install --bypass-low-target-sdk-block FILENAME.apk

    Camera and media


    顾名思义,这就是相机和多媒体相关应用需要注意的修改,在 Android15 中,当达到资源限制时,直接和卸载音频播放现在会使先前打开的直接或卸载音轨无效。在 Android15 之前,如果一个应用在另一个应用正在播放音频时请求直接或卸载音频播放,且已达到资源限制,则该应用将无法打开新的 AudioTrack。从 Android15 开始,当应用请求直接或卸载播放且达到资源限制时,系统会使任何当前打开的 AudioTrack 对象无效,从而阻止满足新曲目请求。

    预测返回动画

    这个也是之前 Android 版本中就加入的新功能,从 Android 15 开始, 预测返回动画的开发者选项已被移除(默认打开)。对于已完全或在 Activity 级别选择启用预测返回手势的应用,现在将显示返回主页、跨任务和跨活动等系统动画 。如果应用受到影响,请执行以下操作:

    1. 确保应用已正确迁移以使用预测返回手势。
    2. 确保 fragment transitions 与预测性返回导航兼容。
    3. 放弃动画和框架转换,改用动画器和 androidx 转换。
    4. 迁移 FragmentManager 未知的返回堆栈。改用由 FragmentManager Navigation 组件管理的返回堆栈。

    /   针对Android15应用的变更   /

    数据同步前台服务超时行为


    Android 15 为以 Android 15 或更高版本为目标平台的应用引入了新的超时行为 dataSync。系统允许应用的 dataSync 服务在 24 小时内总共运行 6 小时,之后系统将调用正在运行的服务的 Service.onTimeout(int, int) 函数。此时,Service 有几秒钟的时间调用 Service.stopSelf()。当 Service.onTimeout() 被调用时,服务不再被视为前台服务。如果服务未调用 Service.stopSelf(),则会报错。

    override fun onTimeout(startId: Int) {
        super.onTimeout(startId)
        stopSelf()
    }

    由于目前 Android 15 还没有正式发布,现在最新版本为 Beta 2 ,在 Beta 2 中,错误显示为 ANR。

    注意:应用的所有前台服务都共享 6 小时的时间限制dataSync。例如,如果应用运行一项dataSync服务 4 小时,然后启动另一项dataSync服务,则第二项服务将只允许运行 2 小时。但是,如果用户将应用置于前台,计时器将重置,应用将有 6 小时的可用时间。如果应用的dataSync前台服务在过去 24 小时内已运行了 6 个小时,则无法启动另一个dataSync前台服务,除非用户将应用调至前台(这会重置计时器)。如果尝试启动另一个dataSync前台服务,系统会抛出 ForegroundServiceStartNotAllowedException 一条错误消息,例如“前台服务类型 dataSync 的时间限制已用尽


    新媒体处理前台服务类型


    Android 15 引入了一种新的前台服务类型:mediaProcessing,此 Service 类型适用于转码媒体文件等操作。例如,媒体应用可能会下载音频文件,并需要将其转换为其他格式才能播放。

    <service
        android:name="Service"
        android:foregroundServiceType="mediaProcessing" />

    可以使用 mediaProcessing 前台服务来确保即使应用在后台,转换仍会继续进行。

    Receiver 启动前台服务的限制


    BOOT_COMPLETED 广播接收器启动前台服务有了新的限制,不得启动以下类型的前台服务:dataSync、camera、mediaPlayback、phoneCall、mediaProjection、microphonemicrophone(此限制自 Android 14 起生效)如果BOOT_COMPLETED接收器尝试启动任何类型的前台服务,系统将抛出异常:ForegroundServiceStartNotAllowedException。

    有SYSTEM_ALERT_WINDOW权限时启动前台服务


    之前 Android 版本中如果应用程序拥有该 SYSTEM_ALERT_WINDOW 权限,即使应用程序当前处于后台,也可以启动前台服务。如果应用以 Android 15 为目标平台,则此豁免范围现在缩小了。应用现在需要获得权限 SYSTEM_ALERT_WINDOW,并且还必须具有可见的 overlay window。也就是说,应用需要先启动一个 TYPE_APPLICATION_OVERLAY 窗口,并且该窗口必须在启动前台服务之前可见。

    如果尝试从后台启动前台服务而不满足这些新要求(并且没有其他豁免),系统将抛出 ForegroundServiceStartNotAllowedException。

    如果声明了 SYSTEM_ALERT_WINDOW 权限并从后台启动前台服务,则可能会受到此更改的影响。如果应用报错 ForegroundServiceStartNotAllowedException,需要检查应用的操作顺序,并确保在应用尝试从后台启动前台服务之前,应用已有一个 overlay window。可以通过调用 View.getWindowVisibility() 来检查 overlay window 当前是否可见,或者也可以重写View.onWindowVisibilityChanged(), 当可见性发生变化时会接收到回调信息。

    阻止与堆栈顶部 UID 不匹配的应用启动


    流氓应用可以在同一任务中启动另一个应用的 Activity,然后将自己叠加在上面,造成该应用的假象。这种“劫持”攻击绕过了当前的后台启动限制,因为这一切都发生在同一可见任务中。为了降低这种风险,Android 15 添加了一项标记,阻止与堆栈顶部 UID 不匹配的应用启动 Activity。要选择加入应用的所有 Activity 的 AndroidManifest.xml文件中的属性,如下代码所示:

    <application android:allowCrossUidActivitySwitchFromBelow="false" >

    如果启用了安全措施,应用程序在完成了自己的任务后可能会返回主页,而不是最后可见的应用程序。

    如果以下所有条件均满足,新的安全措施就会生效:

    1. 执行启动的应用程序以 Android 15 为目标
    2. 任务堆栈顶部的应用程序以 Android 15 为目标
    3. 任何可见的活动都已选择加入新的保护措施


    更安全的Intent


    Android 15 引入了新的安全措施,使 Intent 更安全、更强大。这些变化旨在防止潜在的漏洞和滥用 Intent,以免被恶意应用利用。Android 15 对 Intent 的安全性进行了两项主要改进:

    • 匹配目标 IntentFilter:针对特定组件的 Intent 必须准确匹配目标的 IntentFilter 规范。如果发送 Intent 以启动另一个应用的 Activity,则目标 Intent 组件需要与接收 Activity 声明的 IntentFilter 保持一致。
    • Intent 必须具有操作:没有操作的 Intent 将不再匹配任何 IntentFilter。这意味着用于启动 Activity或 Service 的 Intent 必须具有明确定义的操作。

    可以启用严格模式来帮助发现应用中的潜在问题:

    fun onCreate() {
        StrictMode.setVmPolicy(VmPolicy.Builder()
            .detectUnsafeIntentLaunch()
            .build()
        )
    }

    Edge-to-edge enforcement


    如果应用以 Android 15 为目标平台,则应用在运行 Android 15 的设备上默认为无边框。这是一项重大变更,可能会对应用的 UI 产生负面影响。

    这是一项重大变更,会影响以下 UI 区域:状态栏、导航栏、三大金刚键、刘海:

    手势控制导航栏

    • 默认透明。
    • 底部偏移被禁用,因此除非应用插入,否则内容将在系统导航栏后面绘制。
    • setNavigationBarColor和R.attr#navigationBarColor已被弃用,并且不会影响手势导航。
    • setNavigationBarContrastEnforced并且 R.attr#navigationBarContrastEnforced继续对手势导航没有影响。

    三大金刚键

    • 不透明度默认设置为 80%,颜色可能与窗口背景相匹配。
    • 底部偏移已禁用,因此除非应用插入,否则内容将在系统导航栏后面绘制。
    • setNavigationBarColor并R.attr#navigationBarColor默认设置为与窗口背景匹配。窗口背景必须是彩色可绘制对象,此默认设置才会应用。此 API 已弃用,但仍会影响金刚键。
    • setNavigationBarContrastEnforced默认情况下为 R.attr#navigationBarContrastEnforced真,这会在金刚键中添加 80% 不透明的背景。

    状态栏

    • 默认透明。
    • 顶部偏移被禁用,因此除非应用插入,否则内容会绘制在状态栏后面。
    • setStatusBarColor和R.attr#statusBarColor已被弃用,并且对 Android 15 没有影响。
    • setStatusBarContrastEnforced和 R.attr#statusBarContrastEnforced已被弃用,但仍对 Android 15 有影响。

    刘海

    layoutInDisplayCutoutMode非浮动窗口必须是 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS。SHORT_EDGES,NEVER并且 DEFAULT被解释为ALWAYS,以便用户不会看到由显示切口引起的黑条并且出现边到边。


    适配Edge-to-edge


    如果应用尚未实现无边框设计,则很有可能受到影响。除了已实现无边框设计的应用场景外,你还应考虑以下事项:

    • 如果应用在 Compose 中使用 Material 3 组件不会受到影响
    • 如果应用在 Compose 中使用 Material 2 组件,需手动设置边距。
    • 如果应用使用 View 和 Material 组件,大多数基于视图的 Material 组件(例如BottomNavigationView、BottomAppBar、 NavigationRailView 等)都可以处理边距。但如果使用 Layout 则需要添加 android:fitsSystemWindows="true"。
    • 对于自定义 Composables,需手动设置边距。
    • 如果正在使用 View 、BottomSheet 、SideSheet 或自定义容器,需使用 ViewCompat.setOnApplyWindowInsetsListener 进行监听设置对应边距。对于 RecyclerView 需添加 clipToPadding="false"。

    其实这个修改很快乐,之前应用想要实现沉浸式其实是一个比较费劲的事,会有各种人、各种文章来教你如何设置,但效果却不尽人意,不是实现不了就是代码很丑陋,导致大部人会在 BaseActivity 中直接写上沉浸式的修改。

    但在 Android 15 中,如果应用想要实现沉浸式的话,你需要做的事是:什么都不做!

    原生 View 适配


    先来写下布局:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#B82828"
        tools:context=".MainActivity">

        <Button
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="#000"
            android:text="Tops" />

        <Button
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_alignParentBottom="true"
            android:background="#000"
            android:text="Bottom" />

        <Button
            android:layout_width="50dp"
            android:layout_height="match_parent"
            android:background="#000"
            android:text="Left" />

        <Button
            android:layout_width="50dp"
            android:layout_height="match_parent"
            android:layout_alignParentEnd="true"
            android:background="#000"
            android:text="Right" />

    </RelativeLayout>

    然后来看下 Activity 中的实现:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
    }

    下面来运行看下:

     
    有点意思了哈,但是顶部状态栏可能会影响操作,底部导航栏也可能会影响操作,这该怎么办呢?

    有两种方案:

    使用 setOnApplyWindowInsetsListener

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_test)
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
    }

    来运行看下:


    没问题,是想要的效果。

    布局中使用 fitsSystemWindows

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#B82828"
        android:fitsSystemWindows="true"
        tools:context=".MainActivity"/>

    运行看了下,和上面效果一致。

    Compose 适配


    直接来看代码吧:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                VanillalceTheme {
                    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                        Box(modifier = Modifier
                            .fillMaxSize()
                            .padding(innerPadding)) {

                            Button {
                                Text(text = "Top")
                            }

                            Button {
                                Text(text = "Bottom")
                            }

                            Button {
                                Text(text = "Left")
                            }

                            Button {
                                Text(text = "Right")
                            }
                        }
                    }
                }
            }
        }
    }

    由于 Scaffold 是 Material 3 的组件,所以它可以获取到内边距,只需要设置下就可以了,上面已经设置,咱们来运行看下:


    那如果不设置 Padding 会发生啥呢?

    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
        Box(modifier = Modifier.fillMaxSize()) {}
        ......
    }

    再来运行下:


    小结


    上面简单介绍了在 Android 15 中原生和 Compose 的适配方式,那之前的版本有没有什么好的方式来实现沉浸式呢?官方给我们提供了一个非常好用的函数:enableEdgeToEdge ,来简单看下吧:

    @JvmName("enable")
    @JvmOverloads
    fun ComponentActivity.enableEdgeToEdge(
        statusBarStyle: SystemBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
        navigationBarStyle: SystemBarStyle = SystemBarStyle.auto(DefaultLightScrim, DefaultDarkScrim)
    ) {
        val view = window.decorView
        val statusBarIsDark = statusBarStyle.detectDarkMode(view.resources)
        val navigationBarIsDark = navigationBarStyle.detectDarkMode(view.resources)
        val impl = Impl ?: if (Build.VERSION.SDK_INT >= 30) {
            EdgeToEdgeApi30()
        } else if (Build.VERSION.SDK_INT >= 29) {
            EdgeToEdgeApi29()
        } else if (Build.VERSION.SDK_INT >= 28) {
            EdgeToEdgeApi28()
        } else if (Build.VERSION.SDK_INT >= 26) {
            EdgeToEdgeApi26()
        } else if (Build.VERSION.SDK_INT >= 23) {
            EdgeToEdgeApi23()
        } else if (Build.VERSION.SDK_INT >= 21) {
            EdgeToEdgeApi21()
        } else {
            EdgeToEdgeBase()
        }.also { Impl = it }
        impl.setUp(
            statusBarStyle, navigationBarStyle, window, view, statusBarIsDark, navigationBarIsDark
        )
        impl.adjustLayoutInDisplayCutoutMode(window)
    }

    代码其实还是之前咱们自己设置那些代码,只不过官方现在帮咱们做了这些事,让咱们省了点事,也挺好,有统一接口之后就不用再怕实现沉浸式了嘛!

    Stable configuration


    如果以 Android 15 或更高版本为目标平台,Configuration 则不再排除系统栏。如果使用 Configuration 进行布局计算,则应根据需要将其替换为更好的替代方案,例如 ViewGroup、WindowInsets 或 WindowMetricsCalculator。

    以下列表描述了受此更改影响的函数:Configuration.screenWidthDp、screenHeightDp 不再排除系统栏(状态栏、导航栏)。

    Configuration.smallestScreenWidthDp 会间接受到 screenWidthDp 和 screenHeightDp 的影响。Configuration.orientation 会受到接近正方形的设备变化的 screenWidthDp 和 screenHeightDp 的影响。Display.getSize(Point) 间接受到 Configuration 变化的影响,不过从 API 级别 30 开始,此功能已弃用。Display.getMetrics() 从 API 级别 33 开始就已经这样工作了。

    TextView 宽度因复杂字母形状而变化


    在以前的 Android 版本中,某些具有复杂形状的草书字体或语言可能会在前一个或下一个字符的区域中绘制字母。在某些情况下,这些字母会在开始或结束位置被截断。从 Android 15 开始,系统 TextView 会分配足够的宽度来绘制此类字母,并允许应用请求左侧的额外填充以防止被截断。可以通过调用 setUseBoundsForWidth 来修改启用或停用。由于添加左内边距可能会导致现有布局错位,因此即使针对 Android 15 或更高版本的应用,默认情况下也不会添加内边距。不过,可以通过调用添加额外的内边距来防止裁剪 setShiftDrawingOffsetForStartOverhang。


    /   Android15新功能探索   /

    Camera and media


    低光增强


    Android 15 引入了Low Light Boost,这是一种新的自动曝光模式,可用于Camera 2和夜间模式相机扩展程序。Low Light Boost 会在低光条件下自动调整预览流的亮度。这与夜间模式相机扩展程序创建静止图像的方式不同,因为夜间模式会将多张照片组合在一起以创建一张增强的图像。虽然夜间模式非常适合创建静止图像,但它无法创建连续的帧流,但 Low Light Boost 可以。因此,Low Light Boost 启用了新的相机功能,例如:

    • 提供增强的图像预览,以便用户能够更好地构图低光照片。
    • 在弱光环境下扫描二维码。

    如果启用“低光增强”功能,它会在光线较弱时自动打开,在光线较强时自动关闭。

    应用程序可以在低光照条件下记录预览流以保存明亮的视频。

    在使用“低光增强”之前,需要检查设备是否支持此功能。如果可用,“低光增强”是 中列出的曝光模式之一 camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES。(低光增强是其自己的自动曝光设置,因为其他自动曝光设置与“低光增强”执行的预览增亮不兼容。)

    val characteristics = cameraManager.getCameraCharacteristics(cameraId)
    val autoExposureModes =
        characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES)!!
    val lowLightBoostSupported = autoExposureModes.contains(
            CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY)

    if (lowLightBoostSupported) {
      // Enable Low Light Boost (next section)
    } else {
      // Proceed without Low Light Boost
    }

    要在 Camera2 中启用低光增强功能,需要设置 CaptureRequest.CONTROL_AE_MODE为 ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY。

    val captureRequestBuilder = camera.createCaptureRequest(
      CameraDevice.TEMPLATE_PREVIEW)
    if (isLowLightBoostAvailable(cameraId)) {
      captureRequestBuilder.set(
        CaptureRequest.CONTROL_AE_MODE,
        CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
      )
    }
    // other capture request params

    session.setRepeatingRequest(
      captureRequestBuilder.build(),
      object : CaptureCallback() {
        @Override
        fun onCaptureCompleted(session: CameraCaptureSession,
            request: CaptureRequest, result: TotalCaptureResult) {
          // verify Low Light Boost AE mode set successfully
          result.get(CaptureResult.CONTROL_AE_MODE) ==
              CameraMetadata.CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
        }
      },
      cameraHandler
    )

    低光增强功能可在低光条件下使预览流变亮,如果环境亮度已经足以进行正常拍摄,则不会产生任何效果。可以通过检查字段来确认低光增强功能当前是否处于活动状态CaptureResult.CONTROL_LOW_LIGHT_BOOST_STATE。

    session.setRepeatingRequest(
      captureRequestBuilder.build(),
      object : CaptureCallback() {
        @Override
        fun onCaptureCompleted(session: CameraCaptureSession,
            request: CaptureRequest, result: TotalCaptureResult) {
          // check if Low Light Boost is active or inactive
          if (result.get(CaptureResult.CONTROL_LOW_LIGHT_BOOST_STATE) ==
            CameraMetadata.CONTROL_LOW_LIGHT_BOOST_STATE_ACTIVE) {
            // Low Light Boost state is active
            // Show Moon Icon
          } else {
            // Low Light Boost state is inactive or AE mode is not set
            // to Low Light Boost
            // Hide Moon Icon
          }
        }
      },
      cameraHandler
    )

    应用内相机控制


    Android 15 添加了新的扩展,以便在支持的设备上更好地控制相机硬件及其算法:先进的闪光灯强度调整,使精确控制闪光灯强度在 SINGLE 和 TORCH 模式捕捉图像。

    HDR 余量控制


    Android 15 会选择适合底层设备功能和面板位深度的 HDR 余量,可以使用 setdesireddhdrheadroom 来控制HDR空间

    响度控制



    Android 15 引入了对 CTA-2075响度标准的支持,以避免音频响度不一致,并确保用户在切换内容时不必不断调整音量。该系统利用输出设备(耳机和扬声器)的已知特性以及 AAC 音频内容中可用的响度元数据来智能地调整音频响度和动态范围压缩级别。

    要启用此功能,需要确保 AAC 内容中提供响度元数据,并在应用中启用平台功能。

    // Media contains metadata of type MPEG_4 OR MPEG_D
    val mediaCodec = …
    val audioTrack = AudioTrack.Builder()
                                .setSessionId(sessionId)
                                .build()
    ...
    // Create new loudness controller that applies the parameters to the MediaCodec
    try {
       val lcController = LoudnessCodecController.create(mSessionId)
       // Starts applying audio updates for each added MediaCodec
    }

    AndroidX media3 ExoPlayer 也将更新以使用 LoudnessCodecControllerAPI 实现无缝应用程序集成。

    Developer productivity and tools


    OpenJDK 17 更新


    Android 15 继续更新 Android 核心库的工作,以与最新 OpenJDK LTS 版本中的功能保持一致。

    PDF 改进


    Android 15 对 PdfRenderer API 进行了重大改进。应用可以整合高级功能,例如渲染受 密码保护的文件、注释、表单编辑、 搜索和选择复制。支持线性化 PDF 优化,以加快本地 PDF 查看速度并减少资源使用。

    自动语言切换改进


    Android 14 在音频中添加了设备上的多语言识别功能,可以自动在语言之间切换,但这可能会导致单词丢失,尤其是当语言切换时,两个话语之间的停顿较少。Android 15 添加了额外的控件来帮助应用根据其用例调整此切换。EXTRA_LANGUAGE_SWITCH_INITIAL_ACTIVE_DURATION_TIME_MILLIS 将自动切换限制在音频会话的开始处,而 EXTRA_LANGUAGE_SWITCH_MATCH_SWITCHES在定义的切换次数后停用语言切换。

    改进的 OpenType 可变字体 API


    Android 15 提高了 OpenType 可变字体的可用性。可以使用 FontFamily 从可变字体创建实例,而无需指定粗细轴buildVariableFamily。文本渲染器会覆盖轴的值wght以匹配显示的文本。

    val newTypeface = Typeface.CustomFallbackBuilder(
                FontFamily.Builder(
                    Font.Builder(assets, "RobotoFlex.ttf").build())
                        .buildVariableFamily())
        .build()

    精细换行控制


    从 Android 15 开始,TextView 底层换行符可以将给定的文本部分保留在同一行中,以提高可读性。可以使用 <nobreak>在字符串资源中使用此换行符,也可以使用或 标记 createNoBreakSpan来避免单词被连字符连接。

    例如,以下字符串资源不包含换行符,并且会在不合适的位置呈现文本“Pixel 8 Pro。”:

    <resources>
        <string name="pixel8pro">The power and brains behind Pixel 8 Pro.</string>
    </resources>

    相比之下,此字符串资源包含标签<nobreak>,该标签包装短语“Pixel 8 Pro.”并防止换行:

    <resources>
        <string name="pixel8pro">The power and brains behind <nobreak>Pixel 8 Pro.</nobreak></string>
    </resources>

    这些字符串的呈现方式的差异如下图所示:


    文本的布局,其中“Pixel 8 Pro.”未使用<nobreak>。


    文本布局,其中“Pixel 8 Pro.”使用了<nobreak>。

    ApplicationStartInfo API


    在以前的 Android 版本中,应用启动有点神秘。很难确定应用是从冷、温还是热状态启动的。也很难知道应用在各个启动阶段花费了多长时间:fork 进程、调用 onCreate、绘制第一帧等等。Application 实例化类时,无法知道应用是从 Broadcast、ContentProvider、Job、Backup、BootComplete、Alarm,还是一个 Activity。Android 15 上的 ApplicationStartInfo 提供了所有这些功能,甚至更多。甚至可以选择将自己的时间戳添加到流程中。除了收集指标外,还可以使用它来帮助直接优化应用启动;例如,Application 当应用由于广播而启动时,可以去除类中与 UI 相关的库的实例化等等。

    详细的应用程序大小信息


    自 Android 8.0(API 级别 26)以来,Android 已包含一个 StorageStats.getAppBytes API,该 API 将应用的安装大小汇总为单个字节数,即 APK 大小、从 APK 中提取的文件大小以及设备上生成的文件(例如提前 (AOT) 编译的代码)的大小之和。在Android 15 添加了 StorageStats.getAppBytesByDataType API,可以深入了解应用如何使用所有空间,包括 APK 文件拆分、AOT 和加速相关代码、dex 元数据、库和引导配置文件。


    屏幕录制检测


    Android 15 能够检测应用是否正在被录制。每当应用在屏幕录制期间在可见或不可见之间转换时,都会调用回调。

    val mCallback = Consumer<Int> { state ->
      if (state == SCREEN_RECORDING_STATE_VISIBLE) {
        // We're being recorded
      } else {
        // We're not being recorded
      }
    }

    override fun onStart() {
       super.onStart()
       val initialState =
          windowManager.addScreenRecordingCallback(mainExecutor, mCallback)
       mCallback.accept(initialState)
    }

    override fun onStop() {
        super.onStop()
        windowManager.removeScreenRecordingCallback(mCallback)
    }

    扩展了 IntentFilter 功能


    Android 15 内置了对 Intent 进行更精确解析的 UriRelativeFilterGroup,其中包含一组 UriRelativeFilter 对象,这些对象形成一组 Intent 必须分别满足的匹配规则,包括 URL 查询参数、URL 片段以及阻止或排除规则。

    <intent-filter>
      <action android:name="android.intent.action.VIEW" />
      <category android:name="android.intent.category.BROWSABLE" />
      <data android:scheme="http" />
      <data android:scheme="https" />
      <data android:domain="astore.com" />
      <uri-relative-filter-group>
        <data android:pathPrefix="/auth" />
        <data android:query="region=na" />
      </uri-relative-filter-group>
      <uri-relative-filter-group android:allow="false">
        <data android:pathPrefix="/auth" />
        <data android:query="mobileoptout=true" />
      </uri-relative-filter-group>
      <uri-relative-filter-group android:allow="false">
        <data android:pathPrefix="/auth" />
        <data android:fragmentPrefix="faq" />
      </uri-relative-filter-group>
    </intent-filter>

    查询用户最近选择的“精选照片访问”


    当应用程序被授予部分媒体访问权限时,可以只突出显示最近选择的照片和视频。这个功能可以改善频繁请求访问照片和视频的应用程序的用户体验。如果要使用此功能,请在通过 ContentResolver 查询 MediaStore 时启用 QUERY_ARG_LATEST_SELECTION_ONLY 参数。

    val externalContentUri = MediaStore.Files.getContentUri("external")

    val mediaColumns = arrayOf(
       FileColumns._ID,
       FileColumns.DISPLAY_NAME,
       FileColumns.MIME_TYPE,
    )

    val queryArgs = bundleOf(
       // Return only items from the last selection (selected photos access)
       QUERY_ARG_LATEST_SELECTION_ONLY to true,
       // Sort returned items chronologically based on when they were added to the device's storage
       QUERY_ARG_SQL_SORT_ORDER to "${FileColumns.DATE_ADDED} DESC",
       QUERY_ARG_SQL_SELECTION to "${FileColumns.MEDIA_TYPE} = ? OR ${FileColumns.MEDIA_TYPE} = ?",
       QUERY_ARG_SQL_SELECTION_ARGS to arrayOf(
           FileColumns.MEDIA_TYPE_IMAGE.toString(),
           FileColumns.MEDIA_TYPE_VIDEO.toString()
       )
    )

    小部件预览更新


    在 Android 15 之前,提供小部件选择器预览的唯一方法是指定静态图片或布局资源。这些预览通常与放置在主屏幕上的实际小部件的外观有很大不同。此外,无法使用 Jetpack Glance 创建静态资源,因此 Glance 开发人员必须对其小部件进行屏幕截图或创建 XML 布局才能获得小部件预览。

    Android 15 增加了对生成预览的支持。这意味着应用小部件提供商可以生成预览RemoteViews以用作选择器预览,而不是静态资源。


    PUSH API


    应用可以通过 PUSH API 提供生成的预览。应用可以在其生命周期的任何时间点提供预览,并且不会收到主机提供预览的明确请求。预览将保留在 中AppWidgetService,主机可以按需请求预览。以下示例加载 XML 小部件布局资源并将其设置为预览:

    AppWidgetManager.getInstance(appContext).setWidgetPreview(
       ComponentName(
           appContext,
           SociaLiteAppWidgetReceiver::class.java
       ),
       AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN,
       RemoteViews("com.example", R.layout.widget_preview)
    )

    1. 小部件提供程序可随时调用。提供的预览与其他提供程序信息一起setWidgetPreview保存。AppWidgetService
    2. setWidgetPreview通过回调通知主机已更新预览 AppWidgetHost.onProvidersChanged。作为响应,小部件主机将重新加载其所有提供商信息。
    3. 当显示小部件预览时,会检查 AppWidgetProviderInfo.generatedPreviewCategories,如果所选类别可用,则调用AppWidgetManager.getWidgetPreview返回此提供程序的已保存预览。


    何时调用 setWidgetPreview


    由于没有回调来提供预览,因此应用可以选择在运行时随时发送预览。更新预览的频率取决于小部件的使用情况。

    以下列表描述了预览用例的两大类别:

    • 在其小部件预览中显示真实数据,例如个性化或最新信息。这些提供商可以在用户登录或在其应用中完成初始配置后设置预览。此后,可以设置定期任务以他们选择的节奏更新预览。这种类型的小部件的示例可以是照片、日历、天气或新闻小部件。
    • 在预览或快捷操作小部件中显示静态信息的提供程序,不显示任何数据。这些提供程序可以在应用首次启动时设置一次预览。此类小部件的示例包括驱动器快捷操作小部件。

    应用可以在中心模式选择器上显示静态预览,但在 Launcher 上显示真实信息。

    画中画


    Android 15 在画中画 (PiP) 方面引入了新的变化,确保进入 PiP 模式时过渡更加流畅。这对于将 UI 元素叠加在主 UI 之上并进入 PiP 的应用非常有利。

    使用 onPictureInPictureModeChanged 回调来定义切换叠加界面元素可见性的逻辑。当 PiP 进入或退出动画完成时,会触发此回调。从 Android 15 开始,该类 PictureInPictureUiState 包含一个新状态。

    使用此新 UI 状态,以 Android 15 为目标平台的应用将在画中画动画启动后立即 观察到Activity#onPictureInPictureUiStateChanged调用的回调 。当应用处于画中画模式时,有许多 UI 元素与应用无关,例如包含建议、即将播放的视频、评分和标题等信息的视图或布局。当应用进入画中画模式时,使用回调隐藏这些 UI 元素。当应用从画中画窗口进入全屏模式时,使用回调取消 隐藏这些元素,如以下示例所示:isTransitioningToPip()onPictureInPictureUiStateChanged``onPictureInPictureModeChanged

    override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {
            if (pipState.isTransitioningToPip()) {
              // Hide UI elements
            }
        }
    override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
            if (isInPictureInPictureMode) {
              // Unhide UI elements
            }
        }

    这种不相关 UI 元素(对于 PiP 窗口)的快速可见性切换有助于确保更流畅、无闪烁的 PiP 进入动画。

    为通知渠道设置振动效果


    Android 15 支持使用 通过渠道为传入通知设置丰富的振动 NotificationChannel.setVibrationEffect ,因此用户无需查看设备即可区分不同类型的通知。这意味着可以设置长震动、短震动、间歇震动来区别不同的通知消息。

    封面屏幕支持


    应用可以声明一个属性 COMPAT_SMALL_COVER_SCREEN_OPT_IN ,Android 15 会使用该属性允许 Application 或 Activity 显示在受支持的可翻转设备的小封面屏幕上。这些屏幕太小,不能被视为 Android 应用的兼容目标,但应用可以选择支持它们,从而使应用可以在更多地方使用。


    这个新功能对折叠屏非常友好,现在也有很多类似的折叠屏,应用也可以做些适配。

    /   Android15时间线及总结   /


    目前 Android 15 处于 Beta2 阶段,现在是六月份,按照谷歌的时间线来看八月份之后 Android 15 的正式版就要发布了,虽然现在还不是正式版本,但 Beta 版本中 API 等是不会变化了,剩下的就是性能优化和小修小补了。

    思来想去,写这篇文章感慨万千,一年一次的 Android 大版本更新,新功能的发布、无用功能的废弃,像是在说广大的 Android 开发者,有用的留下,无用的废弃。。。

    推荐阅读:
    我的新书,《第一行代码 第3版》已出版!
    原创:写给初学者的Jetpack Compose教程,用derivedStateOf提升性能
    Android FCM 推送详解,出海应用必备

    欢迎关注我的公众号
    学习技术或投稿


    长按上图,识别图中二维码即可关注
    继续滑动看下一个
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存